# PropertyMappingResolver.java

    package org.codefilarete.stalactite.engine.configurer.dslresolver;

    import java.util.Map;
    import java.util.Set;
    import java.util.function.Consumer;
    import java.util.stream.Stream;
    import javax.annotation.Nullable;

    import org.codefilarete.reflection.AccessorChainMutator;
    import org.codefilarete.reflection.AccessorDefinition;
    import org.codefilarete.reflection.ReadWriteAccessorChain;
    import org.codefilarete.reflection.ReadWritePropertyAccessPoint;
    import org.codefilarete.reflection.ValueAccessPoint;
    import org.codefilarete.reflection.ValueAccessPointComparator;
    import org.codefilarete.reflection.ValueAccessPointMap;
    import org.codefilarete.reflection.ValueAccessPointSet;
    import org.codefilarete.stalactite.dsl.MappingConfigurationException;
    import org.codefilarete.stalactite.dsl.naming.ColumnNamingStrategy;
    import org.codefilarete.stalactite.engine.configurer.builder.embeddable.EmbeddableLinkage;
    import org.codefilarete.stalactite.engine.configurer.builder.embeddable.EmbeddableMappingConfiguration;
    import org.codefilarete.stalactite.engine.configurer.model.Entity.AbstractPropertyMapping;
    import org.codefilarete.stalactite.engine.configurer.model.Entity.PropertyMapping;
    import org.codefilarete.stalactite.engine.configurer.model.Entity.ReadOnlyPropertyMapping;
    import org.codefilarete.stalactite.sql.ddl.Size;
    import org.codefilarete.stalactite.sql.ddl.structure.Column;
    import org.codefilarete.stalactite.sql.ddl.structure.Table;
    import org.codefilarete.stalactite.sql.statement.SQLStatement.BindingException;
    import org.codefilarete.stalactite.sql.statement.binder.ColumnBinderRegistry;
    import org.codefilarete.stalactite.sql.statement.binder.ParameterBinder;
    import org.codefilarete.stalactite.sql.statement.binder.ParameterBinderRegistry.EnumBindType;
    import org.codefilarete.tool.Reflections;
    import org.codefilarete.tool.bean.Objects;
    import org.codefilarete.tool.collection.Arrays;
    import org.codefilarete.tool.collection.KeepOrderSet;

    import static org.codefilarete.stalactite.engine.configurer.builder.embeddable.EmbeddableMappingConfiguration.fromEmbeddableMappingConfiguration;
    import static org.codefilarete.tool.Nullable.nullable;
    import static org.codefilarete.tool.collection.Iterables.stream;

    /**
     * Engine that converts mapping definition of a {@link EmbeddableMappingConfiguration} to a {@link Map} of {@link AbstractPropertyMapping}s.
     * It collects:
     * - direct properties (owned by the {@link EmbeddableMappingConfiguration})
     * - the embedded properties of the embedded configuration
     * - the properties found on inheritance by the mapped superclass definition (until no more mapped superclass is found)
     * - inherited properties of embedded configuration
     *
     * Whereas it consumes an {@link EmbeddableMappingConfiguration} it doesn't mean that its goal is to manage embedded beans of an entity: as its
     * name says, it's aimed at collecting mapping of any beans, without the entity part (understanding identification and inheritance which is
     * {@link AggregateMetadataResolver}'s work).
     *
     * @author Guillaume Mary
     * @see #resolve(org.codefilarete.stalactite.dsl.embeddable.EmbeddableMappingConfiguration, Table, ColumnNamingStrategy)
     */
    public class PropertyMappingResolver<C, T extends Table<T>> {

    	private final ColumnBinderRegistry columnBinderRegistry;

    	public PropertyMappingResolver(ColumnBinderRegistry columnBinderRegistry) {
    		this.columnBinderRegistry = columnBinderRegistry;
    	}

    	/**
    	 * Converts mapping definition of a {@link EmbeddableMappingConfiguration} into a simple {@link Map}
    	 *
    	 * @return a bean that stores some {@link Map}s representing the definition of the mapping declared by the {@link EmbeddableMappingConfiguration}
    	 */
    	public Set<AbstractPropertyMapping<C, ?, T>> resolve(org.codefilarete.stalactite.dsl.embeddable.EmbeddableMappingConfiguration<C> mainMappingConfiguration,
    	                                                     T targetTable,
    	                                                     ColumnNamingStrategy columnNamingStrategy) {
    		return resolve(fromEmbeddableMappingConfiguration(mainMappingConfiguration),
    				targetTable,
    				columnNamingStrategy);
    	}

    	public Set<AbstractPropertyMapping<C, ?, T>> resolve(EmbeddableMappingConfiguration<C> mainMappingConfiguration,
    	                                                     T targetTable,
    	                                                     ColumnNamingStrategy columnNamingStrategy) {
    		Set<AbstractPropertyMapping<C, ?, T>> result = new KeepOrderSet<>();

    		InternalProcessor<C> internalProcessor = new InternalProcessor<>(targetTable, columnNamingStrategy);
    		// converting direct mapping
    		ValueAccessPointMap<C, String, ValueAccessPoint<C>> overriddenColumnNames = new ValueAccessPointMap<>();
    		ValueAccessPointMap<C, Size, ValueAccessPoint<C>> overriddenColumnSizes = new ValueAccessPointMap<>();
    		ValueAccessPointMap<C, Column<T, ?>, ValueAccessPoint<C>> overriddenColumns = new ValueAccessPointMap<>();
    		ValueAccessPointSet<C, ?> excludedProperties = new ValueAccessPointSet<>();
    		internalProcessor.includeMapping(mainMappingConfiguration,
    				overriddenColumnNames,
    				overriddenColumnSizes,
    				overriddenColumns,
    				excludedProperties);
    		// adding insets
    		result.addAll(includeInsets(targetTable, columnNamingStrategy, mainMappingConfiguration));

    		// importing embeddable mapped superclass mapping
    		EmbeddableMappingConfiguration<C> mappedSuperClassConfiguration = (EmbeddableMappingConfiguration<C>) mainMappingConfiguration.getMappedSuperClassConfiguration();
    		while (mappedSuperClassConfiguration != null) {
    			internalProcessor.includeMapping(mappedSuperClassConfiguration,
    					overriddenColumnNames, overriddenColumnSizes, overriddenColumns, excludedProperties);
    			// adding insets
    			result.addAll(includeInsets(targetTable, columnNamingStrategy, mappedSuperClassConfiguration));
    			mappedSuperClassConfiguration = (EmbeddableMappingConfiguration<C>) mappedSuperClassConfiguration.getMappedSuperClassConfiguration();
    		}

    		result.addAll(internalProcessor.result);

    		return result;
    	}

    	private <E> Set<AbstractPropertyMapping<C, ?, T>> includeInsets(T targetTable, ColumnNamingStrategy columnNamingStrategy, EmbeddableMappingConfiguration<C> mainMappingConfiguration) {
    		Set<AbstractPropertyMapping<C, ?, T>> result = new KeepOrderSet<>();
    		mainMappingConfiguration.<E>getInsets().forEach(inset -> {
    			InternalProcessor<E> internalProcessorForEmbeddedBeans = new InternalProcessor<>(targetTable, columnNamingStrategy);
    			internalProcessorForEmbeddedBeans.includeMapping(inset.getConfiguration(),
    					inset.getOverriddenColumnNames(),
    					inset.getOverriddenColumnSizes(),
    					(ValueAccessPointMap<E, Column<T, ?>, ValueAccessPoint<E>>) (ValueAccessPointMap) inset.getOverriddenColumns(),
    					inset.getExcludedProperties());
    			// shifting the embedded configuration by the inset accessor
    			internalProcessorForEmbeddedBeans.result.forEach(mappingPawn -> {
    				ReadWritePropertyAccessPoint<C, E> prefix = inset.getAccessor();
    				AbstractPropertyMapping<C, ?, T> propertyMapping;
    				if (mappingPawn instanceof ReadOnlyPropertyMapping) {
    					propertyMapping = shiftMapping(prefix, (ReadOnlyPropertyMapping) mappingPawn);
    				} else {
    					propertyMapping = shiftMapping(prefix, (PropertyMapping) mappingPawn);
    				}
    				result.add(propertyMapping);
    			});
    		});
    		return result;
    	}

    	private <X, Y> ReadOnlyPropertyMapping<C, Y, T> shiftMapping(ReadWritePropertyAccessPoint<C, X> prefix, ReadOnlyPropertyMapping<X, Y, T> mapping) {
    		AccessorChainMutator<C, X, Y> shiftedAccessor = new AccessorChainMutator<>(Arrays.asList(prefix), mapping.getAccessPoint());
    		return new ReadOnlyPropertyMapping<>(new ReadWriteAccessorChain<>(shiftedAccessor), mapping.getColumn(), mapping.isSetByConstructor(), mapping.getReadConverter(), mapping.isUnique());
    	}

    	private <X, Y> PropertyMapping<C, Y, T> shiftMapping(ReadWritePropertyAccessPoint<C, X> prefix, PropertyMapping<X, Y, T> mapping) {
    		ReadWriteAccessorChain<C, X, Y> shiftedAccessor = new ReadWriteAccessorChain<>(Arrays.asList(prefix), mapping.getAccessPoint());
    		return new PropertyMapping<>(shiftedAccessor, mapping.getColumn(), mapping.isSetByConstructor(), mapping.getReadConverter(), mapping.getWriteConverter(), mapping.isUnique());
    	}

    	/**
    	 * Internal engine driven by {@link #resolve(org.codefilarete.stalactite.dsl.embeddable.EmbeddableMappingConfiguration, Table, ColumnNamingStrategy)} method.
    	 * Made to store the result in another class than the main one and to decouple configuration from the processing.
    	 *
    	 * @author Guillaume Mary
    	 */
    	protected class InternalProcessor<E> {

    		private final T targetTable;
    		private final ColumnNamingStrategy columnNamingStrategy;

    		private final Set<AbstractPropertyMapping<E, ?, T>> result = new KeepOrderSet<>();

    		protected InternalProcessor(T targetTable, ColumnNamingStrategy columnNamingStrategy) {
    			this.targetTable = targetTable;
    			this.columnNamingStrategy = columnNamingStrategy;
    		}

    		protected <O> void includeMapping(EmbeddableMappingConfiguration<E> mappingConfiguration,
    		                                  ValueAccessPointMap<E, String, ValueAccessPoint<E>> overriddenColumnNames,
    		                                  ValueAccessPointMap<E, Size, ValueAccessPoint<E>> overriddenColumnSizes,
    		                                  ValueAccessPointMap<E, Column<T, ?>, ValueAccessPoint<E>> overriddenColumns,
    		                                  ValueAccessPointSet<E, ?> excludedProperties) {

    			Stream<EmbeddableLinkage<E, O>> linkageStream = mappingConfiguration.<O>getPropertiesMapping().stream()
    					.filter(linkage -> !excludedProperties.contains(linkage.getAccessor()));

    			linkageStream.forEach(linkage -> {
    				Column<T, O> overriddenColumn = (Column<T, O>) overriddenColumns.get(linkage.getAccessor());
    				String columnName = nullable(overriddenColumn)
    						.map(Column::getName)
    						.getOr(() -> determineColumnName(linkage, overriddenColumnNames.get(linkage.getAccessor())));
    				Size columnSize = nullable(overriddenColumn)
    						.map(Column::getSize)
    						.getOr(() -> determineColumnSize(linkage, overriddenColumnSizes.get(linkage.getAccessor())));
    				assertMappingIsNotAlreadyDefinedByInheritance(linkage, columnName, mappingConfiguration);
    				AbstractPropertyMapping<E, O, T> propertyMapping = createMapping(
    						linkage,
    						columnName,
    						columnSize,
    						overriddenColumn
    				);
    				result.add(propertyMapping);
    			});
    		}

    		protected <O> String determineColumnName(EmbeddableLinkage<E, O> linkage, @Nullable String overriddenColumName) {
    			return nullable(overriddenColumName)
    					.elseSet(linkage::getColumnName)
    					.elseSet(linkage::getFieldName)
    					.elseSet(() -> columnNamingStrategy.giveName(AccessorDefinition.giveDefinition(linkage.getAccessor())))
    					.get();
    		}

    		protected <O> Size determineColumnSize(EmbeddableLinkage<E, O> linkage, @Nullable Size overriddenColumSize) {
    			return nullable(overriddenColumSize).elseSet(linkage::getColumnSize).get();
    		}

    		/**
    		 * Includes a mapping of a single property.
    		 * @param linkage the user-defined mapping of a property
    		 * @param columnName the column name to use for this property
    		 * @param columnSize the column size to use for this property, will fallbacks to a default one if not given
    		 * @param overriddenColumn the column to use for this property, if not null, then the column name and size are ignored
    		 * @return a coupled accessor and column
    		 * @param <O> the property type
    		 */
    		private <O> AbstractPropertyMapping<E, O, T> createMapping(EmbeddableLinkage<E, O> linkage,
    		                                                           String columnName,
    		                                                           @Nullable Size columnSize,
    		                                                           @Nullable Column<T, O> overriddenColumn) {
    			Column<T, O> column = nullable(overriddenColumn).getOr(() -> addColumnToTable(
    					linkage,
    					linkage.getExtraTable() != null ? linkage.getExtraTable() : targetTable,
    					columnName,
    					columnSize)
    			);

    			ensureColumnBindingInRegistry(linkage, column);

    			AbstractPropertyMapping<E, O, T> propertyMapping;
    			if (!linkage.isReadonly()) {
    				propertyMapping = new PropertyMapping<>(linkage.getAccessor(), column, linkage.isSetByConstructor(), linkage.getReadConverter(), linkage.getWriteConverter(), linkage.isUnique());
    			} else {
    				// we use getWriter() even if getLeft() is sufficient thanks to ReadWritePropertyAccessPoint implementing PropertyMutator
    				// however getWrite() is more precise by removing a encapsulation layer, which potentially reflects much more user declaration
    				propertyMapping = new ReadOnlyPropertyMapping<>(linkage.getAccessor().getWriter(), column, linkage.isSetByConstructor(), linkage.getReadConverter(), linkage.isUnique());
    			}

    			return propertyMapping;
    		}

    		protected <O> void assertMappingIsNotAlreadyDefinedByInheritance(EmbeddableLinkage<E, O> linkage, String columnNameToCheck, EmbeddableMappingConfiguration<E> mappingConfiguration) {
    			DuplicateDefinitionChecker duplicateDefinitionChecker = new DuplicateDefinitionChecker(columnNameToCheck, linkage.getAccessor(), columnNamingStrategy);
    			stream(mappingConfiguration.inheritanceIterable())
    					.flatMap(configuration -> (Stream<EmbeddableLinkage>) configuration.getPropertiesMapping().stream())
    					// not using equals() is voluntary since we want reference checking here to exclude same instance,
    					// since given linkage is one of given mappingConfiguration
    					// (doing as such also prevent equals() method override to break this algorithm)
    					.filter(pawn -> linkage != pawn
    							// only writable properties are concerned by this check : we allow duplicates for readonly properties
    							&& !pawn.isReadonly() && !linkage.isReadonly())
    					.forEach(duplicateDefinitionChecker);
    		}

    		protected <O> Column<T, O> addColumnToTable(EmbeddableLinkage<E, O> linkage, Table targetTable, String columnName, @Nullable Size columnSize) {
    			Class<O> columnType;
    			if (linkage.getColumnType().isEnum()) {
    				if (linkage.getEnumBindType() == null) {
    					columnType = (Class<O>) Enum.class;
    				} else {
    					columnType = linkage.getEnumBindType() == EnumBindType.NAME
    							? (Class<O>) String.class
    							: (Class<O>) Integer.class;
    				}
    			} else {
    				if (linkage.getParameterBinder() != null) {
    					// when a parameter binder is defined, then the column type must be binder one
    					columnType = linkage.getParameterBinder().getColumnType();
    				} else {
    					columnType = linkage.getColumnType();
    				}
    			}
    			// if the user asks for nullability, then we follow his demand, else we rely on the property type: primitive ones are mandatory
    			Boolean isColumnNullable =
    					nullable(linkage.isNullable()).getOr(() -> Reflections.isPrimitiveType(linkage.getColumnType()) ? false : null);
    			return targetTable.addColumn(columnName, columnType, columnSize, isColumnNullable);
    		}

    		protected <O> void ensureColumnBindingInRegistry(EmbeddableLinkage<E, O> linkage, Column<?, O> column) {
    			if (linkage.getColumnType().isEnum()) {
    				EnumBindType enumBindType = Objects.preventNull(linkage.getEnumBindType(), EnumBindType.ORDINAL);
    				columnBinderRegistry.register(column, enumBindType.newParameterBinder((Class<Enum>) linkage.getColumnType()));
    			} else if (linkage.getParameterBinder() != null) {
    				columnBinderRegistry.register(column, (ParameterBinder<O>) linkage.getParameterBinder());
    			} else {
    				try {
    					// assertion to check that column binder is registered : it will throw en exception if the binder is not found
    					columnBinderRegistry.getBinder(column);
    				} catch (BindingException e) {
    					throw new MappingConfigurationException("No binder found for property " + AccessorDefinition.toString(linkage.getAccessor())
    							+ " : neither its column nor its type are registered (" + column.getAbsoluteName() + ", type " + Reflections.toString(column.getJavaType()) + ")", e);
    				}
    			}
    		}
    	}

    	static class DuplicateDefinitionChecker implements Consumer<EmbeddableLinkage> {

    		private final String columnNameToCheck;
    		private final ReadWritePropertyAccessPoint propertyAccessor;
    		private final ColumnNamingStrategy columnNameStrategy;
    		private static final ValueAccessPointComparator VALUE_ACCESS_POINT_COMPARATOR = new ValueAccessPointComparator();

    		DuplicateDefinitionChecker(String columnNameToCheck, ReadWritePropertyAccessPoint propertyAccessor, ColumnNamingStrategy columnNameStrategy) {
    			this.columnNameToCheck = columnNameToCheck;
    			this.propertyAccessor = propertyAccessor;
    			this.columnNameStrategy = columnNameStrategy;
    		}

    		@Override
    		public void accept(EmbeddableLinkage pawn) {
    			ReadWritePropertyAccessPoint accessor = pawn.getAccessor();
    			if (VALUE_ACCESS_POINT_COMPARATOR.compare(accessor, propertyAccessor) == 0) {
    				throw new MappingConfigurationException("Mapping is already defined by method " + AccessorDefinition.toString(propertyAccessor));
    			} else if (columnNameToCheck.equals(pawn.getColumnName())
    					|| columnNameToCheck.equals(columnNameStrategy.giveName(AccessorDefinition.giveDefinition(accessor)))) {
    				throw new MappingConfigurationException("Column '" + columnNameToCheck + "' of mapping '" + AccessorDefinition.toString(propertyAccessor)
    						+ "' is already targeted by '" + AccessorDefinition.toString(pawn.getAccessor()) + "'");
    			}
    		}
    	}
    }